/* @LICENSE(NICTA_CORE) */

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <elf/elf.h>

/* macros */

#define BIT(x) (1 << (x))
#define MASK(n) (BIT(n)-1)
#define IS_ALIGNED(n, b) (!((n) & MASK(b)))
#define ROUND_DOWN(n, b) (((n) >> (b)) << (b))
#define ROUND_UP(n, b) (((((n) - 1) >> (b)) + 1) << (b))

/* constants */

#define PAGE_BITS 12

/* types */

typedef uintptr_t paddr_t;
typedef uintptr_t vaddr_t;

/* prototypes */

void arm_enable_mmu(void);

typedef
    void (*init_kernel_t)(
        paddr_t ui_p_reg_start,
        paddr_t ui_p_reg_end,
        int32_t pv_offset,
        vaddr_t v_entry
    );

/* symbols defined in linker scripts */

extern char _start[];
extern char _end[];
extern char _kernel_elf_start[];
extern char _userland_elf_start[];
extern uint32_t _boot_pd[];

/* functions */

static void arm_map_boot_pd(
    paddr_t ki_p_reg_start,
    vaddr_t ki_v_reg_start
)
{
    uint32_t i;

    /* identity mapping below kernel window */
    for (i = 0; i < (ki_v_reg_start >> 20); i++) {
        _boot_pd[i] = (i << 20)
            | BIT(10) /* kernel-only access */
#ifdef ARMV5
            | BIT(4)  /* must be set for ARMv5 */
#endif
            | BIT(1); /* 1M section */
    }

    /* mapping of kernel window */
    for(i = 0; i < ((-ki_v_reg_start) >> 20); i++) {
        _boot_pd[i + (ki_v_reg_start >> 20)] = ((i << 20) + ki_p_reg_start)
            | BIT(10) /* kernel-only access */
#ifdef ARMV5
            | BIT(4)  /* must be set for ARMv5 */
#endif
            | BIT(1); /* 1M section */
    }
}


/* Determine if two intervals overlap. */
int
regions_overlap(uint32_t startA, uint32_t endA,
        uint32_t startB, uint32_t endB)
{
    if (endA < startB)
        return 0;
    if (endB < startA)
        return 0;
    return 1;
}

/*
 * ELF-loader for ARM systems.
 *
 * We are currently running out of physical memory, with an ELF file for
 * the kernel and an ELF file for the userspace image.
 *
 * Each ELF file indicates a physical load address. Our task is to load
 * each ELF file into its requested physical load address; setup a page
 * directory for the kernel so that when we jump to it; enable the MMU;
 * and then jump to the kernel running out of its virtual address space.
 *
 * Several things could possibly go wrong:
 *
 *  1. The physical load address of the kernel might want to overwrite
 *  this ELF-loader;
 *
 *  2. The physical load address of the userspace image might want to
 *  overwrite this ELF-loader;
 *
 *  3. The physical load address of the kernel and userspace image might
 *  overlap;
 *
 *  4. The physical load addresses of the kernel or userspace images
 *  might not actually be in physical memory.
 *
 *  We check for some of these (1--3), but others problems may go
 *  unnoticed by us.
 */
int main(void)
{
    init_kernel_t init_kernel;
    uint64_t ki_p_reg_start;
    uint64_t ki_p_reg_end;
    uint64_t ui_p_reg_start;
    uint64_t ui_p_reg_end;
    uint64_t ki_v_reg_start;
    uint64_t ki_v_reg_end;
    uint64_t ui_v_reg_start;
    uint64_t ui_v_reg_end;
    int32_t  pv_offset;
    vaddr_t  v_entry;

    printf("ELF-loader image started:   paddr=[%p..%p]\n", _start, _end);

    if (elf_checkFile(_kernel_elf_start)) {
        printf("Kernel image not a valid ELF file!\n");
        abort();
    }
    elf_getMemoryBounds(_kernel_elf_start, 1, &ki_p_reg_start, &ki_p_reg_end);
    elf_getMemoryBounds(_kernel_elf_start, 0, &ki_v_reg_start, &ki_v_reg_end);
    init_kernel = (init_kernel_t)(paddr_t)elf_getEntryPoint(_kernel_elf_start);

    printf(
        "ELF-loading kernel image:   paddr=[%p..%p] vaddr=[%p..%p] v_entry=%p\n",
        (void*)(paddr_t)ki_p_reg_start,
        (void*)(paddr_t)ki_p_reg_end,
        (void*)(vaddr_t)ki_v_reg_start,
        (void*)(vaddr_t)ki_v_reg_end,
        init_kernel
    );

    /* Ensure that the kernel physical load address doesn't overwrite us. */
    if (regions_overlap((paddr_t)ki_p_reg_start, ((paddr_t)ki_p_reg_end) - 1,
                (paddr_t)_start, (paddr_t)_end)) {
        printf("Kernel load address would overlap ELF-loader!\n");
        abort();
    }

    if (!elf_loadFile(_kernel_elf_start, 1)) {
        printf("Error loading kernel image!\n");
        abort();
    }

    if (elf_checkFile(_userland_elf_start)) {
        printf("Userland image not a valid ELF file!\n");
        abort();
    }

    elf_getMemoryBounds(_userland_elf_start, 1, &ui_p_reg_start, &ui_p_reg_end);
    elf_getMemoryBounds(_userland_elf_start, 0, &ui_v_reg_start, &ui_v_reg_end);

    if (!IS_ALIGNED(ui_p_reg_start, PAGE_BITS)) {
        printf("Start of userland image not 4K-aligned!\n");
        abort();
    }

    ui_p_reg_end = ROUND_UP(ui_p_reg_end, PAGE_BITS);
    ui_v_reg_end = ROUND_UP(ui_v_reg_end, PAGE_BITS);
    v_entry = (vaddr_t)elf_getEntryPoint(_userland_elf_start);
    pv_offset = ui_p_reg_start - ui_v_reg_start;

    /* init all userland memory before loading potentially sparse ELF image */
    memset((void*)(paddr_t)ui_p_reg_start, 0, ui_p_reg_end - ui_p_reg_start);

    printf(
        "ELF-loading userland image: paddr=[%p..%p] vaddr=[%p..%p] v_entry=%p\n",
        (void*)(paddr_t)ui_p_reg_start,
        (void*)(paddr_t)ui_p_reg_end,
        (void*)(vaddr_t)ui_v_reg_start,
        (void*)(vaddr_t)ui_v_reg_end,
        (void*)v_entry
    );

    /* Ensure that the userland physical load address doesn't overwrite us or
     * the kernel. */
    if (regions_overlap((paddr_t)ui_p_reg_start, ((paddr_t)ui_p_reg_end) - 1,
                (paddr_t)_start, (paddr_t)_end)) {
        printf("Userland load address would overlap ELF-loader!\n");
        abort();
    }
    if (regions_overlap((paddr_t)ui_p_reg_start, ((paddr_t)ui_p_reg_end) - 1,
                (paddr_t)ki_p_reg_start, ((paddr_t)ki_p_reg_end) - 1)) {
        printf("Userland load address would overlap kernel!\n");
        abort();
    }

    if (!elf_loadFile(_userland_elf_start, 1)) {
        printf("Error loading userland image!\n");
        abort();
    }

    printf("Enabling MMU and paging\n");
    arm_map_boot_pd((paddr_t)ki_p_reg_start, (vaddr_t)ki_v_reg_start);
    arm_enable_mmu();

    printf("Jumping to kernel-image entry point\n");
    init_kernel(
        ui_p_reg_start,
        ui_p_reg_end,
        pv_offset,
        v_entry
    );

    /* we should never get here */
    return -1;
}
